<!--
*@Author: QWP
*@Description: 流程设计板
*@Date 2024-08-14 10:29
-->
<template>
  <div class="my-process-designer">
    <div class="my-process-designer__header">
      <template v-if="!$slots['control-header']">
        <a-button-group key="file-control">
          <a-tooltip title="查看/配置流程基本信息" color="cyan" placement="bottom">
            <a-button type="primary" danger size="mini" @click="showProcessPro" >
              <template #icon><FileTextOutlined /></template>流程属性
            </a-button>
          </a-tooltip>
          <a-tooltip title="查看/保存并发布" color="cyan" placement="bottom">
            <a-button type="primary" size="mini" @click="deploy">
              <template #icon><SaveOutlined /></template>保存并发布
            </a-button>
          </a-tooltip>
          <a-button
            :size="headerButtonSize"
            :type="headerButtonType"
            @click="refFile.click()">
              <template #icon><FolderOpenOutlined /></template>打开文件
            </a-button >
          <a-dropdown placement="bottom">
            <a-button :size="headerButtonSize" :type="headerButtonType">
              <template #icon><ArrowDownOutlined /></template>
              下载文件
            </a-button>
            <template #overlay>
              <a-menu>
                <a-menu-item>
                  <a-button :size="headerButtonSize" type="text" @click="downloadProcessAsXml()" >
                    下载为XML文件
                  </a-button >
                </a-menu-item>
                <a-menu-item>
                  <a-button :size="headerButtonSize" type="text" @click="downloadProcessAsSvg()" >
                    下载为SVG图片
                  </a-button >
                </a-menu-item>
                <a-menu-item>
                  <a-button :size="headerButtonSize" type="text" @click="downloadProcessAsBpmn()" >
                    下载为BPMN文件
                  </a-button >
                </a-menu-item>
              </a-menu>
            </template>
          </a-dropdown>
          <a-dropdown placement="bottom">
            <a-button :size="headerButtonSize" :type="headerButtonType">
              <template #icon><EyeOutlined /></template>
              预览
            </a-button>
            <template #overlay>
              <a-menu>
                <a-menu-item>
                  <a-button :size="headerButtonSize" type="text" @click="previewProcessXML()" >
                    预览XML
                  </a-button >
                </a-menu-item>
                <!-- <a-menu-item>
                  <a-button :size="headerButtonSize" type="text" @click="previewProcessJson()" >
                    预览JSON
                  </a-button >
                </a-menu-item> -->
              </a-menu>
            </template>
          </a-dropdown>
        </a-button-group>
        <a-button-group key="align-control" class="f-margin-l10">
          <a-tooltip title="向左对齐" placement="bottom">
            <a-button
              :size="headerButtonSize"
              class="align align-left"
              @click="elementsAlign('left')">
              <template #icon><AlignLeftOutlined /></template>
            </a-button>
          </a-tooltip>
          <a-tooltip title="向右对齐" placement="bottom">
            <a-button
              :size="headerButtonSize"
              class="align align-right"
              @click="elementsAlign('right')">
              <template #icon><AlignRightOutlined /></template>
            </a-button>
          </a-tooltip>
          <a-tooltip title="向上对齐" placement="bottom">
            <a-button
              :size="headerButtonSize"
              class="align align-top"
              @click="elementsAlign('top')">
              <template #icon><AlignLeftOutlined :rotate="90"/></template>
            </a-button>
          </a-tooltip>
          <a-tooltip title="向下对齐" placement="bottom">
            <a-button
              :size="headerButtonSize"
              class="align align-bottom"
              @click="elementsAlign('bottom')">
              <template #icon><AlignLeftOutlined :rotate="-90"/></template>
            </a-button>
          </a-tooltip>
          <a-tooltip title="水平居中" placement="bottom">
            <a-button
              :size="headerButtonSize"
              class="align align-center"
              @click="elementsAlign('center')">
              <template #icon>
                <AlignCenterOutlined />
              </template>
            </a-button>
          </a-tooltip>
          <a-tooltip title="垂直居中" placement="bottom">
            <a-button
              :size="headerButtonSize"
              class="align align-middle"
              @click="elementsAlign('middle')">
              <template #icon>
                <AlignCenterOutlined :rotate="90"/>
              </template>
            </a-button>
          </a-tooltip>
        </a-button-group>
        <a-button-group key="scale-control" class="f-margin-l10">
          <a-tooltip title="缩小视图" placement="bottom">
            <a-button
              :size="headerButtonSize"
              :disabled="defaultZoom < 0.2"
              @click="processZoomOut()">
              <template #icon>
                <ZoomOutOutlined />
              </template>
            </a-button>
          </a-tooltip>
          <a-button :size="headerButtonSize">{{
            Math.floor(defaultZoom * 10 * 10) + '%'
          }}</a-button>
          <a-tooltip title="放大视图" placement="bottom">
            <a-button
              :size="headerButtonSize"
              :disabled="defaultZoom > 4"
              @click="processZoomIn()">
              <template #icon>
                <ZoomInOutlined />
              </template>
            </a-button>
          </a-tooltip>
          <a-tooltip title="重置视图并居中" placement="bottom">
            <a-button
              :size="headerButtonSize"
              @click="processReZoom()">
              <template #icon>
                <RestOutlined />
              </template>
            </a-button>
          </a-tooltip>
        </a-button-group>
        <a-button-group key="stack-control" class="f-margin-l10">
          <a-tooltip title="撤销" placement="bottom">
            <a-button
              :size="headerButtonSize"
              :disabled="!revocable"
              @click="processUndo()">
              <template #icon>
                <UndoOutlined />
              </template>
            </a-button>
          </a-tooltip>
          <a-tooltip title="恢复" placement="bottom">
            <a-button
              :size="headerButtonSize"
              :disabled="!recoverable"
              @click="processRedo()">
              <template #icon>
                <RedoOutlined />
              </template>
            </a-button>
          </a-tooltip>
          <a-tooltip title="重新绘制" placement="bottom">
            <a-button :size="headerButtonSize" @click="processRestart">
              <template #icon>
                <SyncOutlined />
              </template>
            </a-button>
          </a-tooltip>
        </a-button-group>
      </template>

      <!-- 用于打开本地文件-->
      <input
        type="file"
        id="files"
        ref="refFile"
        style="display: none"
        accept=".xml, .bpmn"
        @change="importLocalFile"
      />
    </div>
    <div class="my-process-designer__container"
      style="overflow: hidden">
      <div class="my-process-designer__canvas" 
        id="bpmnCanvas" 
        ref="bpmnCanvas"></div>
    </div>

    <!-- 预览 -->
    <u-dialog title="预览"
      wrap-class-name="full-modal"
      v-model:open="previewModelVisible"
      destroyOnClose >
      <highlightjs  :language="previewType" :code="previewResult" />
    </u-dialog>

    <!-- 整个的流程属性设置 -->
    <a-drawer title="流程属性"
      v-model:open="dialogVisible"
      @close="cloaseDialog"
      destroyOnClose
      :maskClosable="false"
      width="60%" >
      <div class="drawer-content">
        <ProcessProperties :deploy-m-name="processNewName"
          :process-id="processNewId"
          :deploymentId="deployMId"
          :bpm-element="bpmElement"
          :properties="propertiesParams"
          ref="properties"
        ></ProcessProperties>
      </div>
      <template #footer>
        <div class="drawer-footer f-fr">
          <a-space>
            <a-button type="primary" @click="saveEdit">保存</a-button>
            <a-button @click="cloaseDialog">关闭</a-button>
          </a-space>
        </div>
      </template>
    </a-drawer>
  </div>
</template>

<script lang="ts" setup>
defineOptions({
  name: 'MyProcessDesigner'
})
import { ref, computed, onMounted } from 'vue'
import BpmnModeler from 'bpmn-js/lib/Modeler';
import DefaultEmptyXML from './plugins/defaultEmpty';
import 'bpmn-js/dist/assets/diagram-js.css' 
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'
// 翻译方法
import customTranslate from './plugins/translate/customTranslate';
import translationsCN from './plugins/translate/zh';
// 模拟流转流程
import tokenSimulation from 'bpmn-js-token-simulation';
// 标签解析构建器
// import bpmnPropertiesProvider from "bpmn-js-properties-panel/lib/provider/bpmn";
// 标签解析 Moddle
import camundaModdleDescriptor from './plugins/descriptor/camundaDescriptor.json';
import activitiModdleDescriptor from './plugins/descriptor/activitiDescriptor.json';
import flowableModdleDescriptor from './plugins/descriptor/flowableDescriptor.json';
// 标签解析 Extension
import camundaModdleExtension from './plugins/extension-moddle/camunda';
import activitiModdleExtension from './plugins/extension-moddle/activiti';
import flowableModdleExtension from './plugins/extension-moddle/flowable';
import { deploymentXML, deployWithBPMNJS } from '@bs/process/process';
import { messageC, alertC } from '@/util/common'
import ProcessProperties from "@b/components/designercomponents/ProcessProperties.vue";
import { processProperties } from '@bs/process/flowdesign.js'
import { bpmnEventBus } from '@/util/eventBus'

const props = defineProps({
  value: String, // xml 字符串
  processId: String,
  deployMId: String,
  deployMName: String,
  translations: Object, // 自定义的翻译文件
  options: {
    type: Object,
    default: () => ({})
  }, // 自定义的翻译文件
  additionalModel: [Object, Array], // 自定义model
  moddleExtension: Object, // 自定义moddle
  onlyCustomizeAddi: {
    type: Boolean,
    default: false
  },
  onlyCustomizeModdle: {
    type: Boolean,
    default: false
  },
  simulation: {
    type: Boolean,
    default: true
  },
  keyboard: {
    type: Boolean,
    default: true
  },
  prefix: {
    type: String,
    default: 'activiti'
  },
  events: {
    type: Array,
    default: () => ['element.click']
  },
  headerButtonSize: {
    type: String,
    default: 'small',
    validator: (value) => ['default', 'medium', 'small', 'mini'].indexOf(value) !== -1
  },
  headerButtonType: {
    type: String,
    default: 'primary',
    validator: (value) =>
      ['default', 'primary', 'success', 'warning', 'danger', 'info'].indexOf(value) !== -1
  }
})

const defaultZoom= ref(1);
const previewModelVisible= ref(false);
const simulationStatus= ref(false);
const previewResult= ref('');
const previewType= ref('xml');
const recoverable= ref(false);
const revocable= ref(false);
const dialogVisible= ref(false);
const processNewId= ref('');
const processNewName= ref('');
const bpmElement = ref({})

let bpmnModeler:any = null
const bpmnCanvas = ref()
const refFile = ref()
const properties = ref()

// 整个流程的属性
const propertiesParams = ref<object>({})

// 单项节点的，多人会签集合数据
const multiples = ref({})

const emit = defineEmits([
  'init-finished',
  'destroy',
  'save',
  'commandStack-changed',
  'input',
  'change',
  'canvas-viewbox-changed',
  // eventName.name
  'element-click'
])

const moddleExtensions = computed(() => {
  const Extensions: any = {}
  // 仅使用用户自定义模块
  if (props.onlyCustomizeModdle) {
    return props.moddleExtension || null
  }

  // 插入用户自定义模块
  if (props.moddleExtension) {
    for (let key in props.moddleExtension) {
      Extensions[key] = props.moddleExtension[key]
    }
  }

  // 根据需要的 "流程类型" 设置 对应的解析文件
  if (props.prefix === 'activiti') {
    Extensions.activiti = activitiModdleDescriptor
  }
  if (props.prefix === 'flowable') {
    Extensions.flowable = flowableModdleDescriptor
  }
  if (props.prefix === 'camunda') {
    Extensions.camunda = camundaModdleDescriptor
  }
  return Extensions
})

const additionalModules = computed(() => {
  const Modules = [];
  // 仅保留用户自定义扩展模块
  if (props.onlyCustomizeAddi) {
    if (Object.prototype.toString.call(props.additionalModel) === '[object Array]') {
      return props.additionalModel || [];
    }
    return [props.additionalModel];
  }

  // 插入用户自定义扩展模块
  if (Object.prototype.toString.call(props.additionalModel) == '[object Array]') {
    Modules.push(...(props.additionalModel as any[]))
  } else {
    props.additionalModel && Modules.push(props.additionalModel)
  }

  // 翻译模块
  const TranslateModule = {
    translate: ['value', customTranslate(props.translations || translationsCN)]
  };
  Modules.push(TranslateModule);

  // 模拟流转模块
  // if (props.simulation) {
  //   Modules.push(tokenSimulation);
  // }

  // 根据需要的流程类型设置扩展元素构建模块
  // if (props.prefix === "bpmn") {
  //   Modules.push(bpmnModdleExtension);
  // }
  if (props.prefix === 'camunda') {
    Modules.push(camundaModdleExtension);
  }
  if (props.prefix === 'flowable') {
    Modules.push(flowableModdleExtension);
  }
  if (props.prefix === 'activiti') {
    Modules.push(activitiModdleExtension);
  }

  return Modules;
})

onMounted(() => {
  getBus();

  //新建流程
  if (props.processId == '') {
    initBpmnModeler();
    createNewDiagram(props.value);
  } 
  //编辑流程
  else {
    deploymentXML({ deploymentId: props.deployMId }).then((res) => {
      initBpmnModeler();
      createNewDiagram(res);
    });

    // 获取整改流程的属性
    getProcessProperties();
  }
})

// 获取参数
function getBus() {
  bpmnEventBus.on('collectionMultiple', ({ key, value, clear }) => {
    multiples.value = JSON.parse(sessionStorage.getItem('collectionMultiple') || '{}')
    if(clear == true) {
      if(multiples.value[key]) {
        delete multiples.value[key]
      }
    } else {
      multiples.value[key] = value;
    }
    sessionStorage.setItem('collectionMultiple', JSON.stringify(multiples.value))
  })
}

function initBpmnModeler() {
  if (bpmnModeler) return;
  let data = document.getElementById('bpmnCanvas')
  bpmnModeler = new BpmnModeler({
    container: data,
    width: '100%',
    height: '100%',
    keyboard: props.keyboard ? { bindTo: document } : null,
    additionalModules: additionalModules.value,
    moddleExtensions: moddleExtensions.value,
    ...props.options
  });
  emit('init-finished', bpmnModeler);
  initModelListeners();
}

function initModelListeners() {
  const EventBus = bpmnModeler.get('eventBus');
  // const that = this;
  // 注册需要的监听事件, 将. 替换为 - , 避免解析异常
  props.events.forEach((event) => {
    EventBus.on(event, function (eventObj) {
      let eventName = event.replace(/\./g, '-');
      let element = eventObj ? eventObj.element : null;
      emit(eventName, element, eventObj);
    });
  });

  // 监听图形改变返回xml
  EventBus.on('commandStack.changed', async (event) => {
    try {
      recoverable.value = bpmnModeler.get('commandStack').canRedo();
      revocable.value = bpmnModeler.get('commandStack').canUndo();
      let { xml } = await bpmnModeler.saveXML({ format: true });
      emit('commandStack-changed', event);
      emit('input', xml);
      emit('change', xml);
    } catch (e) {
    }
  });

  // 监听视图缩放变化
  bpmnModeler.on('canvas.viewbox.changed', ({ viewbox }) => {
    emit('canvas-viewbox-changed', { viewbox });
    const { scale } = viewbox;
    defaultZoom.value = Math.floor(scale * 100) / 100;
  });
}

/* 创建新的流程图 */
async function createNewDiagram(xml) {
  // 将字符串转换成图显示出来
  let newId = props.processId || `Process_${new Date().getTime()}`
  processNewId.value = newId;
  let newName = props.processName || `业务流程_${new Date().getTime()}`
  processNewName.value = newName;
  let xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix)
  try {
    let { warnings } = await bpmnModeler.importXML(xmlString)
    if (warnings && warnings.length) {
      warnings.forEach((warn) => console.warn(warn))
    }
  } catch (e: any) {
    console.error(`[Process Designer Warn]: ${e.message || e}`)
  }
  processNewId.value = newId;
}

// 下载流程图到本地
/**
 * @param {string} type
 * @param {*} name
 */
async function downloadProcess(type, name) {
  try {
    // const _this = this;
    // 按需要类型创建文件并下载
    if (type === 'xml' || type === 'bpmn') {
      const { err, xml } = await bpmnModeler.saveXML();
      // 读取异常时抛出异常
      if (err) {
        console.error(`[Process Designer Warn ]: ${err.message || err}`);
      }
      let { href, filename } = setEncoded(type.toUpperCase(), name, xml);
      downloadFunc(href, filename);
    } else {
      const { err, svg } = await bpmnModeler.saveSVG();
      // 读取异常时抛出异常
      if (err) {
        return console.error(err);
      }
      let { href, filename } = setEncoded('SVG', name, svg);
      downloadFunc(href, filename);
    }
  } catch (e) {
    console.error(`[Process Designer Warn ]: ${e.message || e}`);
  }
  // 文件下载方法
  function downloadFunc(href, filename) {
    if (href && filename) {
      let a = document.createElement('a');
      a.download = filename; //指定下载的文件名
      a.href = href; //  URL对象
      a.click(); // 模拟点击
      URL.revokeObjectURL(a.href); // 释放URL 对象
    }
  }
}

// 根据所需类型进行转码并返回下载地址
function setEncoded(type, filename = 'diagram', data) {
  const encodedData = encodeURIComponent(data);
  return {
    filename: `${filename}.${type}`,
    href: `data:application/${
      type === 'svg' ? 'text/xml' : 'bpmn20-xml'
    };charset=UTF-8,${encodedData}`,
    data: data
  };
}

// 保存并发布
async function deploy() {
  if(Object.values(propertiesParams.value).length == 0) {
    messageC("请先设置并保存流程属性!");
    return
  }
  let _xml;
  let _svg;
  const result = await bpmnModeler.saveXML();
  const {err, xml}=result;
  if (err) {
    console.error(err);
  }
  _xml = xml;
  // 会签的集合
  if(Object.keys(multiples.value).length != 0) {
    propertiesParams.value['variablemap'] = JSON.stringify(multiples.value);
  }
  deployWithBPMNJS({ 
    stringBPMNXml: _xml,
    processProperties: JSON.stringify(propertiesParams.value)
  }).then((res) => {
    if (res.error == '200') {
      messageC('发布成功', 'success');
    } else {
      messageC('发布失败：' + res.result, 'warning');
    }
  });
}

// 加载本地文件
function importLocalFile() {
  const file = refFile.value.files[0];
  const reader = new FileReader();
  reader.readAsText(file);
  reader.onload = function () {
    let xmlStr = this.result;
    createNewDiagram(xmlStr);
  };
}

// 获取整个流程的属性
function getProcessProperties()  {
  processProperties({deploymentId: props.deployMId}).then((res)=>{
    if(!props.deployMId) {
      return
    }
    if(res.error=='200'){
      propertiesParams.value = res.result
      if(propertiesParams.value['variablemap']) {
        sessionStorage.setItem('collectionMultiple', propertiesParams.value['variablemap'])
      }
    }
  });
}

/* ------------------------------------------------ refs methods ------------------------------------------------------ */
function downloadProcessAsXml() {
  downloadProcess('xml');
}

function downloadProcessAsBpmn() {
  downloadProcess('bpmn');
}

function downloadProcessAsSvg() {
  downloadProcess('svg');
}

function processRedo() {
  bpmnModeler.get('commandStack').redo();
}
function processUndo() {
  bpmnModeler.get('commandStack').undo();
}

function processZoomIn(zoomStep = 0.1) {
  let newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100;
  if (newZoom > 4) {
    throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4');
  }
  defaultZoom.value = newZoom;
  bpmnModeler.get('canvas').zoom(defaultZoom.value);
}

function processZoomOut(zoomStep = 0.1) {
  let newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100;
  if (newZoom < 0.2) {
    throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2');
  }
  defaultZoom.value = newZoom;
  bpmnModeler.get('canvas').zoom(defaultZoom.value);
}

function processZoomTo(newZoom = 1) {
  if (newZoom < 0.2) {
    throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2');
  }
  if (newZoom > 4) {
    throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4');
  }
  defaultZoom.value = newZoom;
  bpmnModeler.get('canvas').zoom(newZoom);
}

function processReZoom() {
  defaultZoom.value = 1;
  bpmnModeler.get('canvas').zoom('fit-viewport', 'auto');
}

function processRestart() {
  recoverable.value = false;
  revocable.value = false;
  createNewDiagram(null);
}

function elementsAlign(align) {
  const Align = bpmnModeler.get('alignElements');
  const Selection = bpmnModeler.get('selection');
  const SelectedElements = Selection.get();
  if (!SelectedElements || SelectedElements.length <= 1) {
    messageC('请按住 Ctrl 键选择多个元素对齐', "warning");
    return;
  }
  alertC('自动对齐可能造成图形变形，是否继续？', "confirm").then(() => Align.trigger(SelectedElements, align));
}

/*-----------------------------    方法结束     ---------------------------------*/
function previewProcessXML() {
  bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
    previewResult.value = xml;
    previewType.value = 'xml';
    previewModelVisible.value = true;
  });
}

function parseXML(node) {
  const data = {};
  if (node.nodeType === 1) {
    if (node.hasAttributes()) {
      for (let i = 0; i < node.attributes.length; i++) {
        const attribute = node.attributes[i];
        data['@' + attribute.nodeName] = attribute.nodeValue;
      }
    }
    if (node.hasChildNodes()) {
      for (let i = 0; i < node.childNodes.length; i++) {
        const child = node.childNodes[i];
        const nodeName = child.nodeName;
        if (typeof data[nodeName] === 'undefined') {
          data[nodeName] = parseXML(child);
        } else {
          if (typeof data[nodeName].push === 'undefined') {
            const temp = data[nodeName];
            data[nodeName] = [];
            data[nodeName].push(temp);
          }
          data[nodeName].push(parseXML(child));
        }
      }
    }
  } else if (node.nodeType === 3 || node.nodeType === 4) {
    data['#text'] = node.nodeValue;
  }
  return data;
}

function previewProcessJson() {
  // const newConvert = new X2JS();
  bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
    previewResult.value = JSON.stringify(xml, null, 4);
    previewResult.value = parseXML(xml);
    previewType.value = 'json';
    previewModelVisible.value = true;
  });
}
function showProcessPro(){
  bpmElement.value=bpmnModeler;
  let processElement = bpmElement.value.get("elementRegistry").find(el => el.type === "bpmn:Process");
  let businessObject = JSON.parse(JSON.stringify(processElement.businessObject));
  processNewName.value=businessObject["name"];
  dialogVisible.value=true;
}

// 保存整个流程的属性
function saveEdit(){
  propertiesParams.value = properties.value.saveEdit();
  dialogVisible.value = false
}

function cloaseDialog() {
  dialogVisible.value=false;
}

</script>
<style lang="scss" scoped>
#procDes {
  height: calc(100vh - 100px) !important
}
</style>

<style lang="scss">
.my-process-designer {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  padding: 0 5px 10px 0;
  .my-process-designer__header {
    width: 100%;
    min-height: 36px;
    .align {
      position: relative;
      i {
        &:after {
          content: '|';
          position: absolute;
          transform: rotate(90deg) translate(200%, 60%);
        }
      }
    }
  }
  .my-process-designer__container {
    display: inline-flex;
    width: 100%;
    flex: 1;
    max-height: calc(100vh - 130px);
    .my-process-designer__canvas {
      flex: 1;
      height: 100%;
      position: relative;
      background: url('')
        repeat !important;
      div.toggle-mode {
        display: none;
      }
    }
    .my-process-designer__property-panel {
      height: 100%;
      overflow: scroll;
      overflow-y: auto;
      z-index: 10;
      * {
        box-sizing: border-box;
      }
    }
    svg {
      width: 100%;
      height: 100%;
      min-height: 100%;
      overflow: hidden;
    }
  }
  .bjs-powered-by {
    display: none;
  }
}
</style>

